﻿namespace Legend.Core.Tests.Threading.SchedulerTests
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Legend.Threading;
    using NUnit.Framework;
    using FakeItEasy;
    using System.Diagnostics;
    using System.Threading;

    public class SchedulerTestCase
    {
        protected SystemTimeProvider systemTime;
        protected DateTime currentTime;
        protected ITimer timer;
        protected TimerFactory timerFactory;
        protected List<TimeSpan> argumentsForTimerFactory;
        protected Scheduler scheduler;
        protected Action voidAction;
        protected ISynchronizationManager synchronizationManager;

        public SchedulerTestCase()
        {
            systemTime = () => currentTime;
            voidAction = () => Debug.WriteLine("");
        }

        [SetUp]
        public void SetUp()
        {
            this.OnSetUp();
        }

        protected virtual void OnSetUp()
        {
            currentTime = new DateTime(1977, 4, 5);

            timer = A.Fake<ITimer>();
            
            synchronizationManager = A.Fake<ISynchronizationManager>();

            this.argumentsForTimerFactory = new List<TimeSpan>();
            timerFactory = x =>
                {
                    this.argumentsForTimerFactory.Add(x);
                    return timer;
                };

            scheduler = new Scheduler(timerFactory, synchronizationManager, systemTime);
        }
    }

    [TestFixture]
    public class Constructor
    {
        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void should_throw_exception_when_timerFactory_is_null()
        {
            var s = new Scheduler(null, A.Fake<ISynchronizationManager>(), () => DateTime.Now);
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void should_throw_exception_when_systemTimeAccessor_is_null()
        {
            var s = new Scheduler(x => A.Fake<ITimer>(), A.Fake<ISynchronizationManager>(), null);
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void should_throw_exception_when_timerSynchronizationManager_is_null()
        {
            var s = new Scheduler(x => A.Fake<ITimer>(), null, () => DateTime.Now);
        }
    }

    [TestFixture]
    public class Schedule_to_callback_at_specific_time    
        : SchedulerTestCase 
    {
        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void should_throw_exception_when_callBack_is_null()
        {
            scheduler.Schedule(null, currentTime.AddMinutes(1));
        }

        [Test]
        public void should_create_timer_with_one_tick_interval_when_callbackTime_is_now()
        {
            scheduler.Schedule(voidAction, currentTime);
            Assert.That(this.argumentsForTimerFactory, Has.Count.EqualTo(1).And.Some.EqualTo(TimeSpan.FromTicks(1)));
        }

        [Test]
        public void should_create_timer_with_one_tick_interval_when_callbackTime_is_in_the_past()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(-1));
            
            Assert.That(this.argumentsForTimerFactory, Has.Count.EqualTo(1).And.Some.EqualTo(TimeSpan.FromTicks(1)));
        }

        [Test]
        public void should_create_timer_with_interval_set_to_difference_between_now_and_scheduled_event()
        {
            scheduler.Schedule(voidAction, currentTime.AddHours(1));

            Assert.That(this.argumentsForTimerFactory, Has.Count.EqualTo(1).And.Some.EqualTo(TimeSpan.FromHours(1)));
        }

        [Test]
        public void should_call_start_on_timer_when_new_timer_is_created()
        {
            scheduler.Schedule(voidAction, currentTime.AddHours(1));
            
            A.CallTo(() => timer.Start()).MustHaveHappened();
        }

        [Test]
        public void should_not_create_new_timer_when_registering_action_to_occur_after_currently_first_scheduled_action()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            scheduler.Schedule(voidAction, currentTime.AddMinutes(2));

            Assert.That(this.argumentsForTimerFactory.Where(x => x == TimeSpan.FromHours(1)).ToList(), Has.Count.EqualTo(0));
        }
    }

    [TestFixture]
    public class when_timer_elapsed
        : SchedulerTestCase
    {
        [Test]
        public void should_call_callBack()
        {
            bool wasCalled = false;

            scheduler.Schedule(() => wasCalled = true, currentTime.AddHours(1));
            
            timer.Elapsed += Raise.With(EventArgs.Empty).Now;
            
            Assert.IsTrue(wasCalled);
        }

        [Test]
        public void should_dispose_current_timer()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            timer.Elapsed += Raise.With(EventArgs.Empty).Now;

            A.CallTo(() => timer.Dispose()).MustHaveHappened();
        }

        [Test]
        public void should_create_new_timer_with_the_correct_time_set_when_more_actions_are_scheduled()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            scheduler.Schedule(voidAction, currentTime.AddMinutes(5));

            currentTime = currentTime.AddMinutes(1);
            
            timer.Elapsed += Raise.WithEmpty().Now;

            Assert.That(this.argumentsForTimerFactory, Has.Some.EqualTo(TimeSpan.FromMinutes(1)));
        }

        [Test]
        public void should_start_timer_when_more_actions_are_scheduled()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            scheduler.Schedule(voidAction, currentTime.AddMinutes(5));

            currentTime = currentTime.AddMinutes(1);
            
            timer.Elapsed += Raise.WithEmpty().Now;

            A.CallTo(() => this.timer.Start()).MustHaveHappened(Repeated.Exactly.Twice);
        }

        [Test]
        public void should_acquire_write_lock_on_timerSynchronizationManager()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            currentTime = currentTime.AddMinutes(1);

            timer.Elapsed += Raise.WithEmpty().Now;

            A.CallTo(() => synchronizationManager.EnterWriteLock()).MustHaveHappened(Repeated.Exactly.Twice);
        }

        [Test]
        public void should_release_write_lock_on_timerSynchronizationManager()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            currentTime = currentTime.AddMinutes(1);

            timer.Elapsed += Raise.WithEmpty().Now;

            A.CallTo(() => synchronizationManager.ExitWriteLock()).MustHaveHappened();
        }

        [Test]
        public void should_listen_to_any_timer_once_only()
        {
            bool firstActionWasCalled = false;
            bool secondActionWasCalled = false;

            MultiRaiseTimer currentTimer = null;
            var manager = new MonitorSynchronizationManager(new object());

            var scheduler = new Scheduler(
                x => { currentTimer = new MultiRaiseTimer(); return currentTimer; },
                manager,
                () => currentTime);

            scheduler.Schedule(() => firstActionWasCalled = true, currentTime.AddMinutes(1));
            scheduler.Schedule(() => secondActionWasCalled = true, currentTime.AddMinutes(2));

            currentTime = currentTime.AddMinutes(1);

            currentTimer.Raise(5);
            
            Assert.IsTrue(firstActionWasCalled);
            Assert.IsFalse(secondActionWasCalled);

            currentTimer.Raise(1);

            Assert.IsTrue(secondActionWasCalled);
        }

        private class MultiRaiseTimer
            : ITimer
        {
            public void Raise(int times)
            {
                Action multiApartment = () =>
                {
                    var running = new List<WaitHandle>();
                    var actions = new List<Action>();
                    for (int i = 0; i < times; i++)
                    {
                        var resetEvent = new ManualResetEvent(false);

                        var t = new Thread(() =>
                        {
                            Thread.Sleep(10);
                            if (this.Elapsed != null)
                            {
                                this.Elapsed(this, EventArgs.Empty);
                            }

                            resetEvent.Set();
                        });

                        t.Start();

                        running.Add(resetEvent);
                        
                        WaitHandle.WaitAll(running.ToArray());
                    }
                };

                multiApartment.BeginInvoke(null, null).AsyncWaitHandle.WaitOne();
            }

            

            public event EventHandler Elapsed;

            public void Start()
            {
                
            }

            public void Stop()
            {
                
            }

            

            

            public void Dispose()
            {
                
            }

            
        }
    }

    [TestFixture]
    public class Dispose
        : SchedulerTestCase 
    {
        [Test]
        public void should_call_dispose_on_timer()
        {
            scheduler.Schedule(voidAction, currentTime.AddHours(1));
            scheduler.Dispose();

            A.CallTo(() => timer.Dispose()).MustHaveHappened();
        }

        [Test]
        public void should_call_dispose_on_timerSynchronizationManager()
        {
            scheduler.Schedule(voidAction, currentTime.AddHours(1));
            scheduler.Dispose();

            A.CallTo(() => synchronizationManager.Dispose()).MustHaveHappened();
        }
    }

    [TestFixture]
    public class Remove
        : SchedulerTestCase
    {
        Action voidAction = () => {
            bool called = false;
            called = true;
        };

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void should_throw_exception_when_callback_is_null()
        {
            scheduler.Remove(null, currentTime);
        }

        [Test]
        public void should_lock_timer_access()
        {
            DateTime callbackTime = currentTime.AddMinutes(1);

            scheduler.Schedule(voidAction, callbackTime);
            scheduler.Remove(voidAction, callbackTime);

            A.CallTo(() => synchronizationManager.EnterWriteLock()).MustHaveHappened(Repeated.Exactly.Twice);
            A.CallTo(() => synchronizationManager.ExitWriteLock()).MustHaveHappened(Repeated.Exactly.Twice);
        }

        [Test]
        public void should_stop_timer_when_the_callback_is_the_sole_next_callback()
        {
            DateTime callbackTime = currentTime.AddMinutes(1);

            scheduler.Schedule(voidAction, callbackTime);
            scheduler.Remove(voidAction, callbackTime);

            A.CallTo(() => timer.Dispose()).MustHaveHappened();
        }

        [Test]
        public void should_not_act_on_timer_elapsed_after_sole_callback_is_removed()
        {
            bool wasCalled = false;
            Action action = () => wasCalled = true;
            DateTime callbackTime = currentTime.AddMinutes(1);

            scheduler.Schedule(action, callbackTime);
            scheduler.Remove(action, callbackTime);

            timer.Elapsed += Raise.WithEmpty().Now;

            Assert.IsFalse(wasCalled);
        }

        [Test]
        public void should_create_new_timer_when_sole_due_callback_was_removed_and_other_calls_exists()
        {
            scheduler.Schedule(voidAction, currentTime.AddMinutes(1));
            scheduler.Schedule(voidAction, currentTime.AddMinutes(2));

            scheduler.Remove(voidAction, currentTime.AddMinutes(1));

            Assert.That(this.argumentsForTimerFactory, Has.Some.EqualTo(TimeSpan.FromMinutes(2)));
        }

        [Test]
        public void should_not_callback_when_call_is_next_due_but_more_calls_are_scheduled_for_same_time()
        {
            bool wasCalled = false;
            Action action = () => wasCalled = true;
            DateTime callbackTime = currentTime.AddMinutes(1);

            scheduler.Schedule(voidAction, callbackTime);
            scheduler.Schedule(action, callbackTime);
            scheduler.Remove(action, callbackTime);

            currentTime = callbackTime;

            timer.Elapsed += Raise.WithEmpty().Now;

            Assert.IsFalse(wasCalled);
        }

        [Test]
        public void should_not_throw_exception_when_removing_non_scheduled_time()
        {
            scheduler.Remove(voidAction, currentTime.AddMinutes(1));
        }
    }
}
